/*
 * Copyright (C) 2007-2008  Hylke van der Schaaf
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, in version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package hextd.core;

import com.kitfox.svg.SVGCache;
import com.kitfox.svg.SVGDiagram;
import com.kitfox.svg.SVGException;
import com.kitfox.svg.SVGUniverse;
import hextd.HexTD;
import hextd.UI.GameBoard;
import hextd.UI.PanelNetConnection;
import hextd.UI.ScaledCanvas;
import hextd.blackBox.BlackBox;
import hextd.core.gameloop.GameLoopManager;
import hextd.hex.Hex;
import hextd.message.GameMessage;
import hextd.message.Message;
import hextd.message.MessageMap;
import hextd.message.MessageMapList;
import hextd.message.MessagePlayerForfeit;
import hextd.message.MessageSettings;
import hextd.message.MessageSettings.settingKey;
import hextd.creep.Creep;
import hextd.tower.JsonTower.optionKey;
import hextd.tower.Tower;
import hextd.tower.TowerFactory;
import hextd.util.Cache;
import hextd.util.MapIO;
import hextd.util.MapListener;
import hextd.util.SettingChangedListener;
import hextd.util.TowerListener;
import hextd.util.WaveStartListener;
import hextd.util.colorSelection;
import hextd.wave.Path;
import hextd.wave.Wave;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.text.html.StyleSheet;

public class ContextNormal extends Context implements MapListener {

	/**
	 * @return the loadedMapInfo
	 */
	public MapInfo getLoadedMapInfo() {
		return loadedMapInfo;
	}

	public enum AuthStatus {

		unknown,
		success,
		failed
	}
	//
	private boolean connected = false;
	private AuthStatus authenticated = AuthStatus.unknown;
	private int nextUniqueSpriteId = 0;
	//
	private String levelTitle = "";
	private String levelInfo = "";
	private String backgroundImageDefault = "http://pkedu.fbt.wur.nl/~hylke/hextdev/images/sunset.jpg";
	private String backgroundImage = "";
	private int livesStart = 20;
	private long creditsStart = 0;
	private int interestStart = 3;
	private int interestIncrease = 3;
	//
	private GameGrid gameGrid;
	private ArrayList<Creep> enemies;
	private ArrayList<Tower> towers;
	private int enemyCount = 0;
	private ArrayList<Path> paths;
	private int wave = 0;
	private ArrayList<Wave> waves;
	private boolean waveActive = false;
	//
	private HexTD mainApp;
	private GameLoopManager gameLoop;
	private GameBoard gameBoard;
	private ScaledCanvas canvas;
	private MapLoaderClient mapLoader;
	//
	public String VERSION;
	private URL contextUrl;
	private URL mapContextUrl;
	public colorSelection colors;
	//
	private ArrayList<ContextListener> contextListeners;
	private ArrayList<TowerListener> towerListeners;
	private ArrayList<WaveStartListener> waveListeners;
	private ArrayList<SettingChangedListener> settingsListeners;
	private SVGUniverse svgUniverse;
	private HashMap<String, URI> svgImages;
	private HashMap<String, StyleSheet> styleSheets;
	private HashMap<String, BufferedImage> images;
	private Cache cache;
	//
	private PanelNetConnection network;

	/** Creates a new instance of Context */
	public ContextNormal() {
		super();
		init();
	}

	public ContextNormal(HexTD mainApp) {
		super();
		init();
		this.mainApp = mainApp;
	}

	private void init() {
		gameGrid = new GameGrid(this);
		contextListeners = new ArrayList<ContextListener>();
		towerListeners = new ArrayList<TowerListener>();
		waveListeners = new ArrayList<WaveStartListener>();
		settingsListeners = new ArrayList<SettingChangedListener>();
		styleSheets = new HashMap<String, StyleSheet>();
		svgImages = new HashMap<String, URI>();
		enemies = new ArrayList<Creep>();
		images = new HashMap<String, BufferedImage>();
		towers = new ArrayList<Tower>();
		paths = new ArrayList<Path>();
		waves = new ArrayList<Wave>();
		cache = new Cache();
	}

	/**
	 * Cleans up the context to free up memory faster.
	 */
	public void cleanup() {
		gameGrid = null;
		removeAllEnemies();
		clearTowers();
		clearPlayers();
		waves.clear();
		paths.clear();
		gameLoop = null;
		gameBoard = null;
		canvas = null;
		blackBox.clear();
		blackBox = null;
		contextListeners.clear();
		towerListeners.clear();
		waveListeners.clear();
		if (svgUniverse != null) {
			svgUniverse.clear();
		}
		svgImages.clear();
		styleSheets.clear();
		images.clear();
		cache.clearCache();
		mapLoader.removeMapListener(this);
		mapLoader = null;
	}

	public void setMapLoader(MapLoaderClient mapLoader) {
		if (this.mapLoader != null) {
			this.mapLoader.removeMapListener(this);
		}
		this.mapLoader = mapLoader;
		this.mapLoader.addMapListener(this);
	}

	@Override
	public void mapIndexLoaded(List<MapInfo> index) {
	}

	@Override
	public void mapLoaded(MapInfo map) {
		synchronized (gameTime) {
			if (getGameResult() == gameResult.waitingForMap) {
				System.out.println("ContextNormal::mapLoaded: got map " + map.mapId + " from server: " + map.title);
				if (loadedMapInfo.mapId == map.mapId) {
					loadMap(map, null);
					finishLoading();

					System.out.println("ContextNormal::mapLoaded: GameState = " + gameState);
					switch (gameState) {
						case replaying:
							mainApp.startReplay();
					}

				} else {
					System.out.println("ContextNormal::mapLoaded: got map " + map.mapId + " from server, but waiting for " + loadedMapInfo.mapId);
				}
			}
		}
	}

	/**
	 *  Clears out all map and game data from the context to make it ready for loading a new map.
	 */
	public void cleanContext() {
		synchronized (gameTime) {
			System.out.println("ContextNormal::cleanContext: gameState:" + gameState.name() + " gameResult:" + gameResult.name());
			gameBoard.stopPaintTimer();
			this.setWaveActive(false);

			gameGrid = null;
			removeAllEnemies();
			clearTowers();
			clearPlayers();

			wave = 0;
			waves.clear();

			paths.clear();
			setLivesStart(20);
			setInterestIncrease(3);
			setInterestStart(3);
			setLevelInfo("");
			setLevelTitle("");
			setBackgroundImage("");

			switch (gameResult) {
				case waitingForMap:
				case loading:
					break;
				default:
					gameResult = gameResult.inactive;
			}

			blackBox.clear();
		}
	}

	@Override
	public boolean loadMap(String maplocation, String expectedMD5) {
		boolean retval = true;
		synchronized (gameTime) {
			System.out.println("ContextNormal::loadMap 0: Loading map " + maplocation);

			boolean loadFromServer = true;
			int mapId = 0;
			try {
				mapId = Integer.parseInt(maplocation);
			} catch (NumberFormatException e) {
				loadFromServer = false;
			}
			if (loadFromServer) {
				if (loadedMapInfo != null && loadedMapInfo.mapId == mapId) {
					retval = loadMap(loadedMapInfo, expectedMD5);
				} else {
					retval = loadMap(mapLoader.getMap(mapId), expectedMD5);
				}
			} else {
				retval = loadMap(mapLoader.getMap(maplocation), expectedMD5);
			}
		}
		return retval;
	}

	@Override
	public boolean loadMap(MapInfo map, String expectedMD5) {
		synchronized (gameTime) {
			loadedMapInfo = map;
			if (map != null) {
				setGameResult(HexTD.GameResult.loading);
				cleanContext();

				if (expectedMD5 != null && expectedMD5.length() > 0) {
					map.expectedMD5 = expectedMD5;
				}

				if (map.getContent() != null) {
					switch (gameState) {
						case mpSelecting:
							HashMap<settingKey, String> mapSettings = new HashMap<settingKey, String>();
							mapSettings.put(settingKey.selectedMap, "" + map.mapId);
							mapSettings.put(settingKey.version, HexTD.VERSION);
							mapSettings.put(settingKey.mapMD5, map.getMapMD5());
							network.transmitSetting(mapSettings);
					}

					if (map.expectedMD5 != null && map.expectedMD5.length() > 0 && !map.getMapMD5().equalsIgnoreCase(map.expectedMD5)) {
						System.err.println("ContextNormal::loadMap: Sync error: Map MD5 does not match. " + map.expectedMD5 + "!=" + map.getMapMD5());
						showAlert("Loaded map is not the same as map used by game starter. Game may not work as expected!");
					}

					System.out.println("ContextNormal::loadMap 1: Loading map (" + map.mapId + ") of size " + map.getContent().length());
					MapIO.loadLevel(this, map);
					finishLoading();
					return true;

				} else {
					setGameResult(HexTD.GameResult.waitingForMap);
					return false;
				}
			}
		}
		return false;
	}

	public void finishLoading() {
		synchronized (gameTime) {
			for (Path p : this.paths) {
				int i = p.getOwner();
				if (i >= 0) {
					this.ensurePlayer(i);
					ContextPlayer cp = getPlayer(i);
					cp.addPath(p);
				}
			}
			for (Tower t : this.towers) {
				t.resetScale();
			}
			gameBoard.recalculateScale();
			this.resetCredits(this.creditsStart, 0);
			this.setInterest(this.interestStart, this.interestIncrease);
			this.resetLives(this.livesStart);
			this.resetScore();
			this.resetWaves();
			this.resetTicks();
			this.gameTime.reset();
			switch (gameState) {
				case mpCatchup:
				case mpJoinRunning:
				case mpWatching:
					setGameResult(HexTD.GameResult.active);
					break;
				default:
					setGameResult(HexTD.GameResult.inactive);
			}

			this.mainApp.mapChanged();
			gameBoard.startPaintTimer();
		}
	}

	@Override
	public MapInfo getMapInfo() {
		return loadedMapInfo;
	}

	public void loadReplay(String replayLog) {
		mainApp.loadReplay(replayLog);
	}

	@Override
	public void endGame() {
		if (getLocalPlayer() != null && getLocalPlayer().isActive()) {
			setGameResult(HexTD.GameResult.won);
			if (getGameState() != HexTD.GameState.mpGameEnd) {
				network.getLocalPlayerInfo().maps.add(loadedMapInfo.mapId);
			}
			network.sendUpdatedPlayerInfo();
			mainApp.gameOver();
		} else {
			setGameResult(HexTD.GameResult.lost);
			mainApp.gameOver();
		}
	}

	public void messageReceived(Message message) {
		//Message message = MessageFactory.GetMessage(line);
		switch (message.getType()) {
			case LoadMap:
				blackBox.clearBacklog();
				blackBox.readLine((GameMessage) message);
				synchronized (gameTime) {
					mainApp.startLevel(false);
				}
				break;

			case MapList:
				mapLoader.parseRemoteIndex((MessageMapList) message);
				break;

			case Map:
				mapLoader.parseRemoteMap((MessageMap) message);
				break;

			case PlayerForfeit:
				((MessagePlayerForfeit) message).setTick(gameTime.getTime());
			// Fallthrough to default!
			default:
				if (message instanceof GameMessage) {
					blackBox.readLine((GameMessage) message);
				} else {
					System.err.println("Context::messageReceived: Not a GameMessage: " + message.getMessage());
				}
				break;
		}
	}

	@Override
	public void messageSend(String line) {
		this.network.sendLine(line);
	}

	public void newConnection(int connection) {
		this.setConnected(true);
		//throw new UnsupportedOperationException("Not yet implemented");
	}

	public void setSettings(HashMap<settingKey, String> table) {
		synchronized (gameTime) {
			for (Entry<settingKey, String> entry : table.entrySet()) {
				switch (entry.getKey()) {
					case selectedMap:
						fireSettingChanged(entry.getKey(), entry.getValue());
						fireSettingChanged(settingKey.ready, Boolean.toString(false));
						switch (gameResult) {
							case inactive:
								final String value = entry.getValue();
								final String md5 = table.get(settingKey.mapMD5);
								loadMap(value, md5);
								break;

							case loading:
								System.err.println("ContextNormal::setSettings: Warning, already loading a map!");
						}
						break;
					case ladderGame:
					case openGame:
					case publicGame:
						fireSettingChanged(entry.getKey(), entry.getValue());
						fireSettingChanged(settingKey.ready, Boolean.toString(false));
						break;
					case pause:
						fireSettingChanged(entry.getKey(), entry.getValue());
						setPause(Boolean.parseBoolean(entry.getValue()), false);
						break;
				}
			}
		}
	}

	@Override
	public void resetMap() {
		System.err.println("ContextNormal::resetMap: WARNING: resetMap needs refactoring");

		this.removeAllEnemies();
		this.clearTowers();
		this.clearPlayers();
		//MapIO.readLevel(this, new BufferedReader(new StringReader(levelData)));
		this.resetCredits(creditsStart, 0);
		this.setInterest(interestStart, interestIncrease);
		this.resetLives(livesStart);
		this.resetScore();
		this.resetWaves();
		this.resetTicks();
		this.gameTime.reset();
	}

	public void clientStarted() {
		//this.mainApp.clientStarted();
	}

	public void highlightHex(Hex h) {
		this.mainApp.highLightHex(h);
	}

	public Hex getHex(int col, int row) {
		return gameGrid.getHex(col, row);
	}

	public void setEnemyCount(int c) {
		this.enemyCount = c;
	}

	@Override
	public int getEnemyCount() {
		return this.enemyCount;
	}

	@Override
	public synchronized int getUniqueSpriteId() {
		return nextUniqueSpriteId++;
	}

	@Override
	public void addEnemies(Collection<Creep> newEnemies) {
		for (int i = enemies.size() - 1; i >=
				0; i--) {
			Creep e = enemies.get(i);
			if (e.isDead()) {
				enemies.remove(i);
				int target = e.getTarget();
				ContextPlayer cp = getPlayer(target);
				if (cp != null) {
					cp.removeTarget(e);
				}

			}
		}
		this.enemies.addAll(newEnemies);
		for (Creep e : newEnemies) {
			int target = e.getTarget();
			ContextPlayer cp = getPlayer(target);
			if (cp != null) {
				cp.addTarget(e);
			}

		}
		this.fireWaveStartedEvent();
	}

	public Creep findClosestCreep(float paintX, float paintY, float maxRange) {
		Creep esFound = null;
		float maxR2 = maxRange * maxRange;
		float closestR2 = Float.MAX_VALUE;
		float r2, dx, dy;
		for (Creep es : this.enemies) {
			if (!es.isDead()) {
				dx = es.getPaintX() - paintX;
				dy =
						es.getPaintY() - paintY;
				r2 =
						dx * dx + dy * dy;
				if (r2 < closestR2) {
					esFound = es;
					closestR2 =
							r2;
				}

			}
		}
		if (closestR2 < maxR2) {
			return esFound;
		} else {
			return null;
		}

	}

	@Override
	public ArrayList<Creep> getEnemies() {
		return this.enemies;
	}

	@Override
	public void enemyDied() {
		enemyCount--;
		mainApp.enemyDied(enemyCount);
		if (enemyCount == 0) {
			payInterest();
			gameLoop.resetGraceTicks();
			if (wave >= waves.size()) {
				blackBox.endGame(-1, false);
			}

		}
	}

	@Override
	public void removeAllEnemies() {
		this.nextUniqueSpriteId = 0;
		if (this.enemyCount > 0) {
			this.mainApp.enemyDied(this.enemyCount);
		}

		this.enemyCount = 0;
		this.enemies.clear();
		for (ContextPlayer p : getPlayers()) {
			p.removeAllTargets();
		}

	}

	public void resetWaves() {
		this.wave = 0;
		this.waveActive = false;
	}

	@Override
	public ArrayList<Wave> getWaves() {
		return this.waves;
	}

	@Override
	public int getWave() {
		return this.wave;
	}

	@Override
	public int getWaveCount() {
		return this.waves.size();
	}

	@Override
	public void nextWave() {
		Wave w = this.waves.get(this.wave);
		removeAllEnemies();

		this.addEnemies(w.getEnemies());
		for (Creep e : this.enemies) {
			e.resetScale();
		}

		this.wave++;
		this.waveActive = true;
		this.setEnemyCount(w.enemyCount());
		this.fireWaveStartedEvent();
		w.clear(); // We've got the creeps, the wave object can free 'em
	}

	@Override
	public void addWaveStartListener(WaveStartListener l) {
		this.waveListeners.add(l);
	}

	@Override
	public void removeWaveStartListener(WaveStartListener l) {
		this.waveListeners.remove(l);
	}

	private void fireWaveStartedEvent() {
		for (int i = 0; i <
				this.waveListeners.size(); i++) {
			this.waveListeners.get(i).waveStarted();
		}

	}

	public void setInfoText(String s) {
		this.mainApp.setInfoText(s, true);
	}

	@Override
	public void setDebug(String string) {
		mainApp.setDebug(string);
	}

	@Override
	public void setStatus(String string) {
		mainApp.setStatus(string);
	}

	public void setPause(boolean b, boolean transmit) {
		mainApp.setPause(b, transmit);
	}

	public void updateInfo() {
		mainApp.updateInfo();
	}

	public void setInterest(int start, int increase) {
		this.interestStart = start;
		this.interestIncrease = increase;
		for (ContextPlayer cp : getPlayers()) {
			cp.setInterestStart(interestStart);
			cp.setInterestIncrease(interestIncrease);
			this.fireInterestChangedEvent(cp);
		}

	}

	@Override
	public void playerDied(ContextPlayer cp) {
		int activePlayers = getActivePlayers();
		activePlayers--;
		setActivePlayers(activePlayers);

		System.out.println("ContextNormal::playerDied: left: " + activePlayers + " players.");
		if (cp != null) {
			cp.setActive(false);
		}

		if (gameState == HexTD.GameState.spPlaying && activePlayers <= 0) {
			setGameState(HexTD.GameState.spGameEnd);
			blackBox.endGame(-1, false);
		} else if ((gameState == HexTD.GameState.checking || gameState == HexTD.GameState.replaying) && activePlayers <= 0) {
			setGameState(HexTD.GameState.checkingEnd);
			blackBox.endGame(-1, false);
		} else if ((gameState == HexTD.GameState.spPlayingWaveless) && activePlayers <= 1) {
			if (getLocalPlayer() != null && getLocalPlayer().isActive()) {
				setGameResult(HexTD.GameResult.won);
			} else {
				setGameResult(HexTD.GameResult.lost);
			}
			setGameState(HexTD.GameState.spGameEnd);
			blackBox.endGame(-1, false);
		} else if ((gameState == HexTD.GameState.mpPlaying || gameState == HexTD.GameState.mpWatching) && activePlayers <= 1) {
			if (getLocalPlayer() != null && getLocalPlayer().isActive()) {
				setGameResult(HexTD.GameResult.won);
			} else {
				setGameResult(HexTD.GameResult.lost);
			}

			setGameState(HexTD.GameState.mpGameEnd);
			blackBox.endGame(-1, true);
		}

		for (Creep e : cp.getTargets()) {
			e.remove();
		}

		for (Tower t : cp.getTowers()) {
			t.queueSell(gameTime.getTime() + actionDelay);
		}

	}

	@Override
	public void addTower(Tower t) {
		towers.add(t);
		Collections.sort(towers, TowerFactory.towerComparator);

		if (towerOptions.containsKey(t.getType())) {
			t.setSetting(optionKey.maxlevel, "" + towerOptions.get(t.getType()).getMaxLevel());
		}
		int owner = t.getOwner();
		ensurePlayer(owner);
		ContextPlayer cp = getPlayer(t.getOwner());
		cp.addTower(t);
		if (t.getOwner() == getLocalPlayerId() && t.getType() == TowerFactory.type.homeBase) {
			mainApp.setHomeBasePanel(t.getButtons());
		}

		fireTowerAddedEvent(t);
	}

	@Override
	public void removeTower(Tower t) {
		towers.remove(t);
		ContextPlayer p = getPlayer(t.getOwner());
		p.removeTower(t);
		fireTowerRemovedEvent(t);
	}

	@Override
	public void newTower(int col, int row, TowerFactory.type t) {
		if (getLocalPlayer().canPay(t.price, true)) {
			blackBox.newTower(-1, col, row, t, this.getLocalPlayerId(), -1, true);
		} else {
			//System.out.println("ContextNormal::newTower: can't afford tower of type " + t + " for user " + getLocalPlayerId());
		}
	}

	@Override
	public void sellTower(Tower t) {
		int[] hexC = t.getHex().getLocation();
		blackBox.sellTower(-1, hexC[0], hexC[1], -1, t.getOwner(), true);
	}

	@Override
	public void clearTowers() {
		this.mainApp.unSelectTower();
		this.mainApp.removeHomeBasePanel();
		Tower t;

		for (int i = this.towers.size() - 1; i >= 0; i--) {
			t = this.towers.get(i);
			Hex hex = t.getHex();
			hex.unSetTower();
			t.doCleanup();
			removeTower(t);
		}

	}

	@Override
	public void nextMode(Tower t, int m, int step) {
		int[] hexC = t.getHex().getLocation();
		blackBox.setMode(-1, hexC[0], hexC[1], m, t.getMode(m) + step, -1, t.getOwner(), true);
	}

	@Override
	public void setMode(Tower t, int m, int v) {
		int[] hexC = t.getHex().getLocation();
		blackBox.setMode(-1, hexC[0], hexC[1], m, v, -1, t.getOwner(), true);
	}

	@Override
	public void addTowerListener(TowerListener l) {
		this.towerListeners.add(l);
	}

	@Override
	public void removeTowerListener(TowerListener l) {
		this.towerListeners.remove(l);
	}

	private void fireTowerAddedEvent(Tower t) {
		for (int i = 0; i <
				this.towerListeners.size(); i++) {
			this.towerListeners.get(i).towerBuild(t);
		}

	}

	private void fireTowerRemovedEvent(Tower t) {
		for (int i = 0; i <
				this.towerListeners.size(); i++) {
			this.towerListeners.get(i).towerRemoved(t);
		}

	}

	public void addContextListener(ContextListener l) {
		this.contextListeners.add(l);
	}

	public void removeContextListener(ContextListener l) {
		this.contextListeners.remove(l);
	}

	@Override
	public void fireMoneyChangedEvent(ContextPlayer player) {
		for (ContextListener l : contextListeners) {
			l.moneyChanged(player);
		}
	}

	@Override
	public void fireLivesChangedEvent(ContextPlayer player) {
		for (ContextListener l : contextListeners) {
			l.livesChanged(player);
		}
	}

	public void fireInterestChangedEvent(ContextPlayer player) {
		for (ContextListener l : contextListeners) {
			l.interestChanged(player);
		}
	}

	@Override
	public void hexChanged() {
		this.gameBoard.hexChanged();
	}

	@Override
	public ArrayList<Path> getPaths() {
		return this.paths;
	}

	@Override
	public Path getPath(int p) {
		if (p < 0) {
			p = 0;
		}

		if (p > this.paths.size() - 1) {
			p = this.paths.size() - 1;
		}

		return this.paths.get(p);
	}

	@Override
	public Collection<Path> getPaths(int player) {
		this.ensurePlayer(player);
		ContextPlayer cp = getPlayer(player);
		if (cp != null) {
			return cp.getPaths();
		} else {
			System.err.println("Context::getLives: Calling getLives on non-existing payer " + player);
			return null;
		}

	}

	@Override
	public void showAlert(String alert) {
		this.mainApp.showAlert(alert);
	}

	public Cache getCache() {
		return this.cache;
	}

	public StyleSheet loadStyleSheet(String name) {
		if (this.styleSheets.containsKey(name)) {
			return this.styleSheets.get(name);
		} else {
			StyleSheet s = new StyleSheet();
			URL u;

			try {
				u = new URL(this.contextUrl, name);
				s.importStyleSheet(u);
			} catch (MalformedURLException ex) {
				ex.printStackTrace();
			}

			this.styleSheets.put(name, s);
			return s;
		}

	}

	public void clearImages() {
		this.images.clear();
	}

	public BufferedImage loadImage(String location) {
		if (this.images.containsKey(location)) {
			return this.images.get(location);
		} else {
			BufferedImage i = null;
			URL u;

			try {
				u = new URL(this.contextUrl, location);
				i =
						ImageIO.read(u);
			} catch (MalformedURLException ex) {
				System.out.println("Context::loadImage: MalformedURLException reading location: " + location);
			} catch (IOException ex) {
				System.out.println("Context::loadImage: IOException reading location: " + location);
			}

			if (i == null) {
				i = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
			}

			this.images.put(location, i);
			return i;
		}

	}

	public URI loadSvg(String name, String location) {
		if (this.svgUniverse == null) {
			this.svgUniverse = SVGCache.getSVGUniverse();
		}

		if (!this.svgImages.containsKey(name)) {
			URL url;
			try {
				url = new URL(this.contextUrl, location);
			} catch (MalformedURLException ex) {
				ex.printStackTrace();
				return null;
			}

			URI uri = this.svgUniverse.loadSVG(url);
			this.svgImages.put(name, uri);
		}

		return this.svgImages.get(name);
	}

	public void renderSvgImage(String name, Graphics2D g, int w, int h) {
		AffineTransform oldXform = g.getTransform();
		URI image = this.svgImages.get(name);
		SVGDiagram diagram = svgUniverse.getDiagram(image);
		Rectangle2D.Double rect = new Rectangle2D.Double();
		diagram.getViewRect(rect);

		AffineTransform scaleXform = new AffineTransform();
		//scaleXform.setToTranslation(50,50);
		scaleXform.setToScale(w / rect.width, h / rect.height);
		g.transform(scaleXform);

		//diagram.setDeviceViewport(new Rectangle(0, 0, w, h));
		diagram.setIgnoringClipHeuristic(true);
		try {
			diagram.render(g);
		} catch (SVGException ex) {
			ex.printStackTrace();
		}

		g.setTransform(oldXform);
	}

	@Override
	public boolean isWaveActive() {
		return waveActive;
	}

	@Override
	public void setWaveActive(boolean waveActive) {
		this.waveActive = waveActive;
	}

	public void setNetwork(PanelNetConnection network) {
		this.network = network;
	}

	public PanelNetConnection getNetwork() {
		return network;
	}

	public GameBoard getGameBoard() {
		return gameBoard;
	}

	public void setGameBoard(GameBoard gameBoard) {
		this.gameBoard = gameBoard;
		this.canvas = gameBoard;
	}

	public void setScaledCanvas(ScaledCanvas canvas) {
		this.canvas = canvas;
	}

	public ScaledCanvas getScaledCanvas() {
		return this.canvas;
	}

	@Override
	public HexTD.GameState getGameState() {
		return gameState;
	}

	@Override
	public void setGameState(HexTD.GameState newGameState) {
		System.out.println("Context::setGameState: " + newGameState);
		gameState = newGameState;
		mainApp.setUI();
	}

	/**
	 * @return the connected
	 */
	public boolean isConnected() {
		return connected;
	}

	public void setConnected(boolean connected) {
		this.connected = connected;
		mainApp.setUI();
	}

	public AuthStatus getAuthenticated() {
		return authenticated;
	}

	public void setAuthenticated(AuthStatus authenticated) {
		this.authenticated = authenticated;
		mainApp.setUI();
	}

	@Override
	public HexTD.GameResult getGameResult() {
		return gameResult;
	}

	@Override
	public void setGameResult(HexTD.GameResult newGameResult) {
		System.out.println("Context::setGameResult: " + newGameResult);
		this.gameResult = newGameResult;
		mainApp.setUI();
	}

	@Override
	public URL getContextUrl() {
		return contextUrl;
	}

	@Override
	public void setContextUrl(URL contextUrl) {
		this.contextUrl = contextUrl;
	}

	@Override
	public URL getMapContextUrl() {
		return mapContextUrl;
	}

	@Override
	public void setMapContextUrl(URL mapContextUrl) {
		this.mapContextUrl = mapContextUrl;
	}

	@Override
	public ArrayList<Tower> getTowers() {
		return towers;
	}

	@Override
	public String getMapLocation() {
		if (loadedMapInfo != null) {
			if (loadedMapInfo.mapId == 0) {
				return "Custom";
			} else {
				return "" + loadedMapInfo.mapId;
			}
		} else {
			return "";
		}
	}

	@Override
	public String getLevelTitle() {
		return levelTitle;
	}

	@Override
	public void setLevelTitle(String levelTitle) {
		this.levelTitle = levelTitle;
	}

	@Override
	public String getLevelInfo() {
		return levelInfo;
	}

	@Override
	public void setLevelInfo(String levelInfo) {
		this.levelInfo = levelInfo;
	}

	@Override
	public String getBackgroundImage(boolean canDefault) {
		if ((backgroundImage == null || backgroundImage.length() == 0) && canDefault) {
			return backgroundImageDefault;
		}

		return backgroundImage;
	}

	@Override
	public void setBackgroundImage(String backgroundImage) {
		this.backgroundImage = backgroundImage;
	}

	@Override
	public int getLivesStart() {
		return livesStart;
	}

	@Override
	public void setLivesStart(int livesStart) {
		this.livesStart = livesStart;
	}

	@Override
	public long getCreditsStart() {
		return creditsStart;
	}

	@Override
	public void setCreditsStart(long creditsStart) {
		this.creditsStart = creditsStart;
	}

	@Override
	public int getInterestStart() {
		return interestStart;
	}

	@Override
	public void setInterestStart(int interestStart) {
		this.interestStart = interestStart;
	}

	@Override
	public int getInterestIncrease() {
		return interestIncrease;
	}

	@Override
	public void setInterestIncrease(int interestIncrease) {
		this.interestIncrease = interestIncrease;
	}

	@Override
	public GameGrid newGameGrid() {
		gameGrid = new GameGrid(this);
		return gameGrid;
	}

	@Override
	public GameGrid getGameGrid() {
		return gameGrid;
	}

	@Override
	public BlackBox getBlackBox() {
		return blackBox;
	}

	@Override
	public void setBlackBox(BlackBox blackBox) {
		this.blackBox = blackBox;
	}

	public void pauseChanged() {
		mainApp.setUI();
	}

	public GameLoopManager getGameLoop() {
		return gameLoop;
	}

	@Override
	public void setGameLoop(GameLoopManager gameLoop) {
		this.gameLoop = gameLoop;
	}

	public void addSettingsListener(SettingChangedListener l) {
		settingsListeners.add(l);
	}

	public void removeSettingsListener(SettingChangedListener l) {
		settingsListeners.remove(l);
	}

	public void fireSettingChanged(MessageSettings.settingKey key, String val) {
		SettingChangedListener[] toArray = settingsListeners.toArray(new SettingChangedListener[settingsListeners.size()]);
		for (SettingChangedListener l : toArray) {
			l.settingChanged(key, val);
		}
	}
}
